﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Mnk.Merger.Code.Common;
using Mnk.Merger.Code.Settings;

namespace Mnk.Merger.Code.Repository
{
    class RepositoryFacade : IRepositoryFacade
    {
        private readonly IProcessExecutor commands;
        private readonly IRepositoryCache cache;
        private readonly MergeConfig config;
        private readonly ISerializer serializer;

        public RepositoryFacade(IProcessExecutor commands, IRepositoryCache cache, MergeConfig config, ISerializer serializer)
        {
            this.commands = commands;
            this.cache = cache;
            this.config = config;
            this.serializer = serializer;
        }

        public string GetLatestMergedRevisions(string workingDirectory, string mergeUrl)
        {
            var info = GetInfo(workingDirectory);
            var relativeUrl = GetRelativeUrl(mergeUrl, info);
            var p = commands.Execute("svn diff", workingDirectory);
            var found = false;
            var alternative = string.Empty;
            foreach (var line in p.StandardOutputLines)
            {
                if (found)
                {
                    if (line.StartsWith("   Merged "))
                    {
                        var parts = line.Substring(10).Split(':');
                        if (string.Equals(parts[0], relativeUrl))
                        {
                            return parts[1].Substring(1);
                        }
                        if (!string.IsNullOrEmpty(alternative)) alternative += ",";
                        alternative += parts[1].Substring(1);
                    }
                    else break;
                }
                if (string.Equals(line, "Modified: svn:mergeinfo") || string.Equals(line, "Added: svn:mergeinfo")) found = true;
            }
            return alternative;
        }

        public SvnInfoEntry GetInfo(string workingDirectory)
        {
            var p = commands.Execute("svn info --xml", workingDirectory);
            return serializer.Deserialize<SvnInfo>(p.StandardOutput).Entry;
        }

        public IList<string> GetMergedRevisions(string workingDirectory, string mergeUrl)
        {
            var p = commands.Execute("svn propget svn:mergeinfo --xml", workingDirectory);
            var entry = serializer.Deserialize<SvnProperties>(p.StandardOutput);
            if (entry.Target != null)
            {
                var properties = entry.Target.Property
                    .Split(new[] {"\n"}, StringSplitOptions.RemoveEmptyEntries)
                    .Select(x=>x.Trim())
                    .ToArray();
                return properties.SelectMany(GetMergedRevisions).ToArray();
            }
            return new List<string>();
        }

        private IList<string> GetMergedRevisions(string target)
        {
            var items = target.Split(new[] {":"}, StringSplitOptions.RemoveEmptyEntries)
                .Last()
                .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries)
                .SelectMany(GetRevisions)
                .ToArray();
            if (!string.IsNullOrEmpty(config.RevisionsToMerge))
            {
                var toMerge = GetRestrictedRevisions();
                items = items.Where(x => !toMerge.Contains(x)).ToArray();
            }
            return items;
        }

        private string[] GetRestrictedRevisions()
        {
            return config.RevisionsToMerge.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).SelectMany(GetRevisions).ToArray();
        }

        public IList<SvnLogEntry> GetCommits(string mergeUrl)
        {
            var items = cache.Get(mergeUrl);
            if (!string.IsNullOrEmpty(config.RevisionsToMerge))
            {
                var toMerge = GetRestrictedRevisions();
                items = items.Where(x => !toMerge.Contains(x.Id)).ToArray();
            }
            return items;
        }

        public ToMergeInfo GetRevisionToMerge(string workingDirectory, string mergeUrl)
        {
            var entities = GetCommits(mergeUrl);
            if (!string.IsNullOrEmpty(config.AuthorsToMerge))
            {
                var authors = config.AuthorsToMerge
                    .Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries);
                entities = entities
                    .Where(x => authors.Contains(x.Author, StringComparer.OrdinalIgnoreCase))
                    .ToArray();
            }
            if (!string.IsNullOrEmpty(config.MessagesToMerge))
            {
                var messages = config.MessagesToMerge
                    .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                entities = entities
                    .Where(x => messages.Any(m => x.Msg.IndexOf(m, StringComparison.OrdinalIgnoreCase) != -1))
                    .ToArray();
            }
            var commits = entities.Select(x => x.Id).ToArray();
            var merged = GetMergedRevisions(workingDirectory, mergeUrl);
            var toMerge =  commits.FirstOrDefault(c => !merged.Any(m => string.Equals(c, m)));
            return new ToMergeInfo
            {
                Total = commits.Length,
                Merged = commits.Intersect(merged).Count(),
                RevisionsToMerge = toMerge
            };
        }

        public bool Merge(string workingDirectory, string mergeUrl, string revision)
        {
            var command = string.Format("svn merge -c{0} {1} --allow-mixed-revisions", revision, mergeUrl);
            if (config.UseDryMerge)
            {
                if (!CheckOutput(command + " --dry-run", workingDirectory)) return false;
            }
            else
            {
                command += " --accept p ";
            }
            return CheckOutput(command, workingDirectory);
        }

        private bool CheckOutput(string command, string workingDirectory)
        {
            var p = commands.Execute(command, workingDirectory);
            var output = p.StandardOutputLines;
            if (output.Any(x => x.StartsWith("C")))
            {
                Console.WriteLine(command);
                Console.WriteLine(string.Join(Environment.NewLine, output));
                Console.WriteLine(p.StandardError);
                return false;
            }
            return true;
        }

        public void Commit(string workingDirectory, string mergeUrl, string revisions)
        {
            var msg = config.MessageDescription + Environment.NewLine + 
                BuildMessage(workingDirectory, mergeUrl, revisions);
            var tmp = Path.GetTempFileName();
            try
            {
                File.WriteAllText(tmp, msg.Replace(Environment.NewLine, "\n"));
                var command = string.Format("svn ci -F {0}", tmp);
                commands.Execute(command, workingDirectory);
            }
            finally
            {
                File.Delete(tmp);
            }
        }

        public void Update(string workingDirectory)
        {
            commands.Execute("svn up", workingDirectory);
        }

        public void Revert(string workingDirectory)
        {
            commands.Execute("svn revert -R .", workingDirectory);
        }

        public void CleanUp(string workingDirectory)
        {
            commands.Execute("svn cleanup", workingDirectory);
        }

        public void Switch(string workingDirectory, string targetSourceUrl)
        {
            commands.Execute("svn switch " + targetSourceUrl, workingDirectory);
        }

        public IDictionary<string, List<string>> GetChangedFiles(string url, string revision, bool additionalFiltering = true)
        {
            var command = string.Format("-r{0}:HEAD", revision);
            var p = commands.Execute(string.Format("svn log \"{0}\" {1} -v --xml", url, command));
            var entities = serializer.Deserialize<SvnLogEntities>(p.StandardOutput);
            if (additionalFiltering)
            {
                if (!string.IsNullOrEmpty(config.MessagesToMerge))
                {
                    var msgs = config.MessagesToMerge.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                    entities.LogEntries = entities.LogEntries.Where(x => msgs.Any(m => x.Msg.Contains(m))).ToList();
                }
            }
            var items = entities.LogEntries.ToDictionary(x=>x.Id, x=>x.SvnPaths.Paths);
            var info = GetInfo(url, Environment.CurrentDirectory);
            var relativeUrlParts = GetRelativeUrl(url, info).Split(new[]{'/'}, StringSplitOptions.RemoveEmptyEntries).Count();
            foreach (var item in items)
            {
                var old = item.Value.ToArray();
                item.Value.Clear();
                foreach (var path in old)
                {
                    item.Value.Add( NormalizePath(path, relativeUrlParts));
                }
            }
            return items;
        }

        private static string NormalizePath(string str, int relativeUrlParts)
        {
            var parts = str
                .Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries)
                .Skip(relativeUrlParts);
            return string.Join("/", parts);
        }

        private static string GetRelativeUrl(string url, SvnInfoEntry info)
        {
            return url.Replace(info.Repository.Root, "");
        }

        private SvnInfoEntry GetInfo(string url, string workingDirectory)
        {
            var p = commands.Execute(string.Format("svn info {0} --xml", url), workingDirectory);
            return serializer.Deserialize<SvnInfo>(p.StandardOutput).Entry;
        }


        private string BuildMessage(string workingDirectory, string mergeUrl, string revisions)
        {
            var info = cache.Get(mergeUrl);
            var sb = new StringBuilder();
            foreach (var revision in GetRevisions(revisions))
            {
                var commit = info.FirstOrDefault(x => string.Equals(x.Id, revision));
                if(commit == null)continue;
                var relativeUrl = GetRelativeUrl(mergeUrl, GetInfo(workingDirectory));
                sb.AppendLine(string.Format("Merged revision(s) {0} from {1}:", 
                    commit.Id, relativeUrl.Substring(1)));
                sb.AppendLine(commit.Msg);
                sb.AppendLine("........");
            }
            if (sb.Length == 0)
            {
                throw new Exception("No revisions info");
            }
            return sb.ToString();
        }

        private static IEnumerable<string> GetRevisions(string revisions)
        {
            var ids = revisions.Split(',').ToList();
            foreach (var id in ids.ToArray())
            {
                var value = id;
                if (value.Contains("*"))
                {
                    ids.Remove(value);
                    value = value.Replace("*", "");
                    ids.Add(value);
                }
                if (!value.Contains("-")) continue;
                var range = value.Split('-');
                if (range.Length != 2) throw new ArgumentException("Invalid revisions range: " + value);
                var start = int.Parse(range[0]);
                var end = int.Parse(range[1]);
                ids.Remove(value);
                for (var i = start; i <= end; i++)
                {
                    ids.Add(i.ToString());
                }
            }

            return ids;
        }
    }
}